/*
 * $Id$
 * CONFIDENTIAL AND PROPRIETARY. (c) 2007 Revolution Health Group LLC. All rights reserved. 
 * This source code may not be disclosed to others, used or reproduced without the written permission of Revolution Health Group.
 *
 */
package org.javason.json;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.beanutils.converters.AbstractArrayConverter;
import org.apache.commons.beanutils.converters.BigDecimalConverter;
import org.apache.commons.beanutils.converters.BigIntegerConverter;
import org.apache.commons.beanutils.converters.BooleanConverter;
import org.apache.commons.beanutils.converters.DoubleConverter;
import org.apache.commons.beanutils.converters.IntegerConverter;
import org.apache.commons.beanutils.converters.LongConverter;
import org.apache.log4j.Logger;
import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONNull;
import net.sf.json.JSONObject;

import org.javason.jsonrpc.impl.DateConverter;
import org.javason.jsonrpc.impl.JsonRpcOperationInvoker;
import org.javason.jsonrpc.impl.XMLGregorianCalendarConverter;
import org.javason.util.ClassUtils;

public class JsonToBeanDeserializer {
	private static final Logger LOG = Logger.getLogger(JsonRpcOperationInvoker.class);
	public static final String VERSION = "$Rev$";
	private BeanUtilsBean _beanUtils;
	
	public JsonToBeanDeserializer() {
		_beanUtils = new BeanUtilsBean();
		_beanUtils.getConvertUtils().register(new BigDecimalConverter(null), BigDecimal.class);
		_beanUtils.getConvertUtils().register(new BigIntegerConverter(null), BigInteger.class);
		_beanUtils.getConvertUtils().register(new IntegerConverter(null), Integer.class);
		_beanUtils.getConvertUtils().register(new DoubleConverter(null), Double.class);
		_beanUtils.getConvertUtils().register(new LongConverter(null), Long.class);
		_beanUtils.getConvertUtils().register(new BooleanConverter(null), Boolean.class);
		_beanUtils.getConvertUtils().register(new XMLGregorianCalendarConverter(null), XMLGregorianCalendar.class);
		_beanUtils.getConvertUtils().register(new DateConverter(null), java.util.Date.class);
		_beanUtils.getConvertUtils().register(new DateConverter(null), java.sql.Date.class);
		// TODO: add more converters here.
	}
	
	public <T> T deserializer(Class<T> beanClazz, JSONObject jsonObj) {
		try {
			return (T) convertType(beanClazz, jsonObj, null);
		} catch (Exception exception) {
			throw new IllegalArgumentException(exception);
		}
	}
    
	public <T> T deserializer(Class<T> clazz, String jsonObj) {
		try {
			return (T) convertType(clazz, jsonObj, null);
		} catch (Exception exception) {
			throw new IllegalArgumentException(exception);
		}
	}
	
	
	protected Object convertType(Class paramType, Object param, Class parameterizedType) 
			throws InstantiationException, IllegalAccessException, JSONException, InvocationTargetException {
		Object result = null;

        if (JSONNull.getInstance().equals(param) == false) {
            Converter possibleConverter = _beanUtils.getConvertUtils().lookup(paramType);
            if (possibleConverter != null && !(possibleConverter instanceof AbstractArrayConverter)) {
                String convertableType = param.toString();
                result = _beanUtils.getConvertUtils().convert(convertableType, paramType);
            } else if (ClassUtils.isArrayType(paramType)) {
                result = convertArray(ensureJSONArray(param), paramType, parameterizedType);
            } else if (paramType.isArray()) {
            	// deserialize to an arraylist
            	List listResult = convertArray(ensureJSONArray(param), ArrayList.class, parameterizedType);
            	// now convert the arraylist to an array of paramType
            	result = listResult.toArray((Object[]) Array.newInstance(paramType.getComponentType(), 0));
            } else if (paramType.isEnum()) {
                result = ClassUtils.findEnumConstant(paramType, param);
            } else if (ClassUtils.isMapType(paramType)) {
            	result = convertMap(param, paramType, parameterizedType);
            } else {
                result = convertBean(paramType, param, parameterizedType);
            } 
        }
        
        return result;
	}
	
	private static JSONArray ensureJSONArray(Object param) throws JSONException {
		JSONArray jsonArrayParam = null;
		if (param instanceof String) { // make a best attempt to convert the string
			jsonArrayParam = JSONArray.fromObject(param.toString());
		} else { // or else just cast it, if it fails it will throw and we'll know somebody passed bogus stuff
			jsonArrayParam = (JSONArray) param;
		}
		return jsonArrayParam;
	}
	
	private static JSONObject ensureJSONObject(Object param) throws JSONException {
		JSONObject jsonObjectParam = null;
		if (param instanceof String) { // make a best attempt to convert the string
			jsonObjectParam = JSONObject.fromObject(param.toString());
		} else { // or else just cast it, if it fails it will throw and we'll know somebody passed bogus stuff
			jsonObjectParam = (JSONObject) param;
		}
		return jsonObjectParam;
	}
	
	
	
	private static void setArrayThroughGetter(Object bean, Method getter, Object value) {
		try {
			List list = (List) getter.invoke(bean);
			list.clear();
			list.addAll((Collection) value);
		} catch (IllegalArgumentException e) {
			LOG.error("Caught exception: ", e);
		} catch (IllegalAccessException e) {
			LOG.error("Caught exception: ", e);
		} catch (InvocationTargetException e) {
			LOG.error("Caught exception: ", e);
		}
	}
	
	private Object convertBean(Class paramType, Object type, Class parameterizedType) 
			throws InstantiationException, IllegalAccessException, JSONException, InvocationTargetException {
		PropertyUtilsBean propertyUtils = new PropertyUtilsBean();
		PropertyDescriptor[] descriptors = propertyUtils.getPropertyDescriptors(paramType);
		Object bean = paramType.newInstance();
		//JSONObject jsonObj = ((JSONObject)type).getJSONObject(paramType.getSimpleName());
		JSONObject jsonObj = (JSONObject)type;
		for (PropertyDescriptor property : descriptors) {
			String key = property.getName();
			if (!key.equals("class") && jsonObj.has(key)) {
				Method setterMethod = property.getWriteMethod();
				Class beanParameterizedType = null;
				boolean setThroughGetter = false;
				if (setterMethod == null && ClassUtils.isArrayType(property.getPropertyType())) {
					setterMethod = property.getReadMethod();
					beanParameterizedType = ClassUtils.getArrayElementType(setterMethod.getGenericReturnType());
					setThroughGetter = true;
				} else {
					beanParameterizedType = ClassUtils.getArrayElementType(setterMethod, 0);
				}
				
				if (setterMethod != null) {
					Object value = null;
					if (!JSONNull.getInstance().equals(jsonObj.get(key))) {
						value = convertType(
							property.getPropertyType(), jsonObj.get(key), beanParameterizedType);
					}
					
					if (!setThroughGetter) {
						_beanUtils.setProperty(bean, key, value);
					} else {
						setArrayThroughGetter(bean, setterMethod, value);
					}
				}
			}
		}
		
		return bean;
	}
	
	private List convertArray(JSONArray jsonArray, Class listType, Class listParameterizedType)
			throws JSONException {
		
		List list = new ArrayList();
		try {
			if (!listType.isInterface()) {
				list = (List) listType.newInstance();
			}

			for (int j = 0; j < jsonArray.size(); j++) {
				if (listParameterizedType != null && 
						(ClassUtils.hasEmptyConstructor(listParameterizedType) || listParameterizedType.isEnum())) {
					
					list.add(convertType(listParameterizedType, jsonArray.get(j), null));
				} else {
					list.add(jsonArray.get(j));
				}
			}
		} catch (Exception e) {
			throw new JSONException(e);
		}
		return list;
	}
	
	private Map convertMap(Object mapParam, Class listType, Class mapParameterizedType) throws JSONException {
		JSONObject jsonObject = ensureJSONObject(mapParam);
		Map result = new HashMap();
		try {
			Iterator iter = jsonObject.keys();
			while (iter.hasNext()) {
				String key = (String) iter.next();
				if (mapParameterizedType != null && 
						(ClassUtils.hasEmptyConstructor(mapParameterizedType) || mapParameterizedType.isEnum())) {
					
					result.put(key, convertType(mapParameterizedType, jsonObject.get(key), null));
				} else {
					result.put(key, jsonObject.get(key));
				}
			}
		} catch (Exception e) {
			throw new JSONException(e);
		}
		return result;
	}
}
